You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
160 lines
4.7 KiB
160 lines
4.7 KiB
<script setup lang="ts">
|
|
import { unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
|
|
import {
|
|
formatOccurredOnDisplay,
|
|
occurredOnToIsoAttr,
|
|
} from '../../../utils/timeline-datetime'
|
|
|
|
definePageMeta({
|
|
layout: 'public',
|
|
title: '时光机',
|
|
})
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const slug = computed(() => route.params.publicSlug as string)
|
|
|
|
function parsePage(raw: unknown): number {
|
|
const n =
|
|
typeof raw === 'string'
|
|
? Number.parseInt(raw, 10)
|
|
: typeof raw === 'number'
|
|
? raw
|
|
: Number.NaN
|
|
if (!Number.isFinite(n) || n < 1) {
|
|
return 1
|
|
}
|
|
return Math.floor(n)
|
|
}
|
|
|
|
const page = ref(parsePage(route.query.page))
|
|
|
|
watch(
|
|
() => [route.params.publicSlug, route.query.page] as const,
|
|
() => {
|
|
page.value = parsePage(route.query.page)
|
|
},
|
|
)
|
|
|
|
type PublicTimelineItem = {
|
|
id?: number
|
|
title?: string | null
|
|
occurredOn?: Date | string | null
|
|
linkUrl?: string | null
|
|
bodyMarkdown?: string | null
|
|
}
|
|
|
|
type Payload = {
|
|
items: PublicTimelineItem[]
|
|
total: number
|
|
page: number
|
|
pageSize: number
|
|
}
|
|
|
|
function timelineItemKey(e: PublicTimelineItem, i: number): string | number {
|
|
return e.id ?? i
|
|
}
|
|
|
|
function onPageChange(p: number) {
|
|
page.value = p
|
|
const query: Record<string, string | string[] | undefined> = { ...route.query }
|
|
if (p > 1) {
|
|
query.page = String(p)
|
|
}
|
|
else {
|
|
delete query.page
|
|
}
|
|
void router.replace({ query })
|
|
}
|
|
|
|
const { data, pending, error } = await useAsyncData(
|
|
() => `public-timeline-${slug.value}-${page.value}`,
|
|
async () => {
|
|
const base = `/api/public/profile/${encodeURIComponent(slug.value)}/timeline`
|
|
const url = page.value > 1 ? `${base}?page=${page.value}` : base
|
|
const res = await $fetch<ApiResponse<Payload>>(url)
|
|
return unwrapApiBody(res)
|
|
},
|
|
{ watch: [slug, page] },
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<UContainer class="py-8 lg:py-10 max-w-6xl">
|
|
<div v-if="pending && !data" class="text-muted py-10">
|
|
加载中…
|
|
</div>
|
|
<UAlert
|
|
v-else-if="error && !data"
|
|
color="error"
|
|
title="无法加载时光机"
|
|
class="my-6"
|
|
/>
|
|
<template v-else-if="data">
|
|
<UEmpty
|
|
v-if="data.total === 0"
|
|
title="暂无时光机记录"
|
|
description="站主尚未发布任何公开动态。"
|
|
/>
|
|
<template v-else>
|
|
<h2 class="text-xs font-semibold uppercase tracking-wider text-muted mb-4">
|
|
时光机
|
|
</h2>
|
|
<ul class="relative space-y-0">
|
|
<li
|
|
v-for="(e, i) in data.items"
|
|
:key="timelineItemKey(e, i)"
|
|
class="relative flex gap-4 pb-6 pl-1 last:pb-0"
|
|
>
|
|
<div
|
|
v-if="i < data.items.length - 1"
|
|
class="absolute left-[11px] top-5 bottom-0 w-px bg-default"
|
|
aria-hidden="true"
|
|
/>
|
|
<div class="relative z-[1] flex shrink-0 flex-col items-center pt-0.5">
|
|
<span class="size-2.5 rounded-full bg-primary ring-4 ring-primary/15" />
|
|
</div>
|
|
<article
|
|
class="min-w-0 flex-1 rounded-xl border border-default bg-elevated/25 px-5 py-5 shadow-sm sm:px-6"
|
|
>
|
|
<div class="flex flex-col gap-1 sm:flex-row sm:items-baseline sm:justify-between sm:gap-4">
|
|
<time
|
|
class="shrink-0 text-xs font-medium tabular-nums text-muted sm:order-2 sm:text-right"
|
|
:datetime="e.occurredOn ? occurredOnToIsoAttr(e.occurredOn) : undefined"
|
|
>{{ formatOccurredOnDisplay(e.occurredOn ?? '') }}</time>
|
|
<h3 class="text-pretty text-lg font-semibold text-highlighted sm:order-1 sm:min-w-0 sm:flex-1">
|
|
{{ e.title }}
|
|
</h3>
|
|
</div>
|
|
<p
|
|
v-if="e.bodyMarkdown && e.bodyMarkdown.trim()"
|
|
class="mt-4 whitespace-pre-wrap border-t border-default/60 pt-4 text-sm leading-relaxed text-default text-pretty"
|
|
>
|
|
{{ e.bodyMarkdown }}
|
|
</p>
|
|
<a
|
|
v-if="e.linkUrl"
|
|
:href="e.linkUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
|
>
|
|
<span>打开链接</span>
|
|
<UIcon name="i-lucide-external-link" class="size-3.5 opacity-80" />
|
|
</a>
|
|
</article>
|
|
</li>
|
|
</ul>
|
|
<div v-if="data.total > 10" class="flex justify-end mt-6">
|
|
<UPagination
|
|
:page="page"
|
|
:total="data.total"
|
|
items-per-page="10"
|
|
size="sm"
|
|
@update:page="onPageChange"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</UContainer>
|
|
</template>
|
|
|